/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.openide.filesystems; import java.io.*; import java.util.*; import java.lang.ref.*; import javax.swing.event.EventListenerList; import org.openide.TopManager; import org.openide.util.WeakListener; import org.openide.util.Utilities; import org.openide.util.enum.*; /** Implementation of the file object for multi file system. * * @author Jaroslav Tulach, */ final class MultiFileObject extends AbstractFolder implements FileChangeListener { /** generated Serialized Version UID */ static final long serialVersionUID = -2343651324897646809L; /** default extension separator */ private static final char EXT_SEP = '.'; /** default path separator */ private static final char PATH_SEP = '/'; /** list of objects that we delegate to and that already * has been created. */ private Set delegates; /** current delegate (the first object to delegate to), never null */ private FileObject leader; /** Reference to lock or null */ private Reference lock; /** listener */ private FileChangeListener weakL; /** Constructor. Takes reference to file system this file belongs to. * * @param fs the file system * @param parent the parent object (folder) * @param name name of the object (e.g. <code>filename.ext</code>) */ public MultiFileObject(MultiFileSystem fs, MultiFileObject parent, String name) { super (fs, parent, name); weakL = WeakListener.fileChange (this, null); update (); } /** Constructor for root. * * @param fs the file system */ public MultiFileObject (MultiFileSystem fs) { this (fs, null, ""); // NOI18N } /** File system. */ public FileSystem getLeaderFileSystem () throws FileStateInvalidException { return leader.getFileSystem(); } /** Updates list of all references. */ private void update () { FileSystem[] arr = getMultiFileSystem ().systems; Set now = delegates == null ? Collections.EMPTY_SET : delegates; HashSet del = new HashSet (arr.length * 2); FileObject led = null; String name = toString (); for (int i = 0; i < arr.length; i++) { if (arr[i] != null) { FileObject fo = arr[i].findResource (name); if (fo != null) { del.add (fo); if (!now.remove (fo)) { // now there yet fo.addFileChangeListener(weakL); } if (led == null) { led = fo; } } } } Iterator it = now.iterator (); while (it.hasNext()) { FileObject fo = (FileObject)it.next (); fo.removeFileChangeListener (weakL); } if (led != null) { // otherwise leave the leader to be last file that represented // this one if (led != this.leader && this.leader != null) { getMultiFileSystem ().notifyMigration (this); } this.leader = led; } this.delegates = del; } /** Getter for the right file system */ private MultiFileSystem getMultiFileSystem () { return (MultiFileSystem)getFileSystem (); } /** Getter for one of children. */ private MultiFileObject getMultiChild (String name) { return (MultiFileObject)getChild (name); } /** Converts the file to be writable. * The file has to be locked! * * @return file object (new leader) that is writable * @exception IOException if the object cannot be writable */ private FileObject writable () throws IOException { MultiFileSystem fs = getMultiFileSystem (); FileSystem single = fs.createWritableOn (toString ()); if (single != leader.getFileSystem()) { // if writing to a file that is not on writable fs => // copy it if (leader.isFolder()) { leader = FileUtil.createFolder (single.getRoot (), toString ()); } else { FileObject folder = FileUtil.createFolder(single.getRoot (), getParent ().toString ()); leader = leader.copy (folder, leader.getName (), leader.getExt ()); } MfLock l = (MfLock)(lock == null ? null : lock.get ()); if (l != null) { // update the lock FileLock prev = l.lock; l.lock = leader.lock (); if (prev != null) { // can be null if we lock file on readonly system prev.releaseLock (); } } } return leader; } /** All objects that are beyond this one. * @return enumeration of FileObject */ private Enumeration delegates () { return getMultiFileSystem ().delegates (toString ()); } /** Method that goes upon list of folders and updates its locks. This is used when * an object is masked which may lead to creation of folders on a disk. * * @param fo folder to check * @exception IOException if something locks cannot be updated */ private static void updateFoldersLock (FileObject fo) throws IOException { while (fo != null) { MultiFileObject mfo = (MultiFileObject)fo; MfLock l = (MfLock)(mfo.lock == null ? null : mfo.lock.get ()); if (l != null) { // the file has been locked => update the lock mfo.writable (); } fo = fo.getParent (); } } // // List // /** Method that allows subclasses to return its children. * * @return names (name . ext) of subfiles */ protected final String[] list () { LinkedList list = new LinkedList (); HashSet mask = new HashSet (37); Iterator it = delegates.iterator(); while (it.hasNext()) { FileObject folder = (FileObject)it.next (); if (folder == null || !folder.isFolder ()) continue; FileObject[] add = folder.getChildren (); for (int j = 0; j < add.length; j++) { String e = add[j].getExt (); String name = add[j].getName (); if (e != null && e.length () != 0) { name = name + '.' + e; } if (name.endsWith (MultiFileSystem.MASK)) { name = name.substring (0, name.length () - MultiFileSystem.MASK.length ()); mask.add (name); } else { list.add (name); } } } if (!mask.isEmpty ()) { list.removeAll (mask); } String[] arr = (String[])list.toArray (new String[list.size()]); return arr; } /** When refreshing, also update the state of delegates. */ protected synchronized void refresh ( String add, String remove, boolean fire, boolean expected ) { update (); super.refresh (add, remove, fire, expected); } /** Method to create a file object for given subfile. * @param name of the subfile * @return the file object */ protected final AbstractFolder createFile (String name) { return new MultiFileObject (getMultiFileSystem (), this, name); } // // Info // /* Test whether this object is a folder. * @return true if the file object is a folder (i.e., can have children) */ public boolean isFolder () { return leader.isFolder (); } /* * Get last modification time. * @return the date */ public java.util.Date lastModified() { return leader.lastModified (); } /* Test whether this object is a data object. * This is exclusive with {@link #isFolder}. * @return true if the file object represents data (i.e., can be read and written) */ public boolean isData () { return leader.isData (); } /* Test whether this file can be written to or not. * @return <CODE>true</CODE> if file is read-only */ public boolean isReadOnly () { MultiFileSystem fs = getMultiFileSystem (); if (fs.isReadOnly ()) { return true; } if (leader.isReadOnly ()) { // if we can make it writable then nothing try { FileSystem simple = fs.createWritableOn (toString ()); return simple == leader.getFileSystem (); } catch (IOException e) { return true; } } return false; } /* Get the MIME type of this file. * The MIME type identifies the type of the file's contents and should be used in the same way as in the <B>Java * Activation Framework</B> or in the {@link java.awt.datatransfer} package. * <P> * The default implementation calls {@link FileUtil#getMIMEType}. * * @return the MIME type textual representation, e.g. <code>"text/plain"</code> */ public String getMIMEType () { return leader.getMIMEType (); } /* Get the size of the file. * @return the size of the file in bytes or zero if the file does not contain data (does not * exist or is a folder). */ public long getSize () { return leader.getSize (); } /* Get input stream. * @return an input stream to read the contents of this file * @exception FileNotFoundException if the file does not exists or is invalid */ public InputStream getInputStream () throws java.io.FileNotFoundException { return leader.getInputStream (); } /* Get output stream. * @param lock the lock that belongs to this file (obtained by a call to * {@link #lock}) * @return output stream to overwrite the contents of this file * @exception IOException if an error occures (the file is invalid, etc.) */ public synchronized OutputStream getOutputStream (FileLock lock) throws java.io.IOException { testLock (lock); MfLock l = (MfLock)lock; // this can also change lock in l.lock FileObject fo = writable (); return fo.getOutputStream (l.lock); } /* Lock this file. * @return lock that can be used to perform various modifications on the file * @throws FileAlreadyLockedException if the file is already locked */ public synchronized FileLock lock () throws IOException { if (lock != null) { FileLock f = (FileLock)lock.get (); if (f != null) { // System.out.println ("Already locked: " + this); // NOI18N throw new FileAlreadyLockedException(); } } FileLock l; // create the lock FileSystem single = getMultiFileSystem ().createWritableOn (toString ()); if (single == leader.getFileSystem()) { // lock exactly the leader's file l = new MfLock (leader.lock ()); } else { // do not lock the leader l = new MfLock (); } lock = new WeakReference (l); // Thread.dumpStack (); // System.out.println ("Locking file: " + this); // NOI18N return l; } /** Tests the lock if it is valid, if not throws exception. * @param l lock to test * @return the lock to use on leader */ private FileLock testLock (FileLock l) throws java.io.IOException { if (lock == null || lock.get () != l) { FSException.io ("EXC_InvalidLock", toString (), getMultiFileSystem ().getDisplayName ()); // NOI18N } return ((MfLock)l).lock; } // [???] Implicit file state is important. /* Indicate whether this file is important from a user perspective. * This method allows a file system to distingush between important and * unimportant files when this distinction is possible. * <P> * <em>For example:</em> Java sources have important <code>.java</code> files and * unimportant <code>.class</code> files. If the file system provides * an "archive" feature it should archive only <code>.java</code> files. * @param b true if the file should be considered important */ public void setImportant (boolean b) { Enumeration en = delegates (); while (en.hasMoreElements ()) { FileObject fo = (FileObject)en.nextElement (); fo.setImportant (b); } if (!b) { getMultiFileSystem ().markUnimportant (this); } } /* Get the file attribute with the specified name. * @param attrName name of the attribute * @return appropriate (serializable) value or <CODE>null</CODE> if the attribute is unset (or could not be properly restored for some reason) */ public Object getAttribute(String attrName) { Enumeration en = delegates (); while (en.hasMoreElements ()) { FileObject fo = (FileObject)en.nextElement (); Object obj = fo.getAttribute (attrName); if (obj != null) { return obj; } } return null; } /* Set the file attribute with the specified name. * @param attrName name of the attribute * @param value new value or <code>null</code> to clear the attribute. Must be serializable, although particular file systems may or may not use serialization to store attribute values. * @exception IOException if the attribute cannot be set. If serialization is used to store it, this may in fact be a subclass such as {@link NotSerializableException}. */ public void setAttribute(String attrName, Object value) throws IOException { writable ().setAttribute (attrName, value); } /* Get all file attribute names for this file. * @return enumeration of keys (as strings) */ public Enumeration getAttributes() { return leader.getAttributes (); } /* Create a new folder below this one with the specified name. Fires * <code>fileCreated</code> event. * * @param name the name of folder to create (without extension) * @return the new folder * @exception IOException if the folder cannot be created (e.g. already exists) */ public synchronized FileObject createFolder (String name) throws IOException { MultiFileSystem fs = getMultiFileSystem (); if (fs.isReadOnly()) { FSException.io ("EXC_FSisRO", fs.getDisplayName ()); // NOI18N } if (isReadOnly()) { FSException.io ("EXC_FisRO", name, fs.getDisplayName ()); // NOI18N } String fullName = toString () + PATH_SEP + name; FileSystem simple = fs.createWritableOn (fullName); // create FileUtil.createFolder (simple.getRoot (), fullName); // try to unmask if necessary MultiFileSystem.unmaskFile (simple, fullName); refresh (name, null); MultiFileObject fo = getMultiChild (name); if (fo == null) { // system error throw new FileStateInvalidException (FileSystem.getString ("EXC_ApplicationCreateError", toString (), name)); } if (hasListeners ()) { fileCreated0(new FileEvent(fo), false); } return fo; } /* Create new data file in this folder with the specified name. Fires * <code>fileCreated</code> event. * * @param name the name of data object to create (should not contain a period) * @param ext the extension of the file (or <code>null</code> or <code>""</code>) * * @return the new data file object * @exception IOException if the file cannot be created (e.g. already exists) */ public synchronized FileObject createData (String name, String ext) throws IOException { MultiFileSystem fs = getMultiFileSystem (); if (fs.isReadOnly()) { FSException.io ("EXC_FSisRO", fs.getDisplayName ()); // NOI18N } if (isReadOnly()) { FSException.io ("EXC_FisRO", name, fs.getDisplayName ()); // NOI18N } String n = "".equals (ext) ? name : name + EXT_SEP + ext; // NOI18N String fullName = toString () + PATH_SEP + n; FileSystem simple = fs.createWritableOn (fullName); // create FileUtil.createData (simple.getRoot (), fullName); // try to unmask if necessary MultiFileSystem.unmaskFile (simple, fullName); refresh (n, null); MultiFileObject fo = getMultiChild (n); if (fo == null) { // system error throw new FileStateInvalidException (FileSystem.getString ("EXC_ApplicationCreateError", toString (), n)); } if (hasListeners ()) { fileCreated0(new FileEvent(fo), true); } return fo; } /* Renames this file (or folder). * Both the new basename and new extension should be specified. * <p> * Note that using this call, it is currently only possible to rename <em>within</em> * a parent folder, and not to do moves <em>across</em> folders. * Conversely, implementing file systems need only implement "simple" renames. * If you wish to move a file across folders, you should call {@link FileUtil#moveFile}. * @param lock File must be locked before renaming. * @param name new basename of file * @param ext new extension of file (ignored for folders) */ public void rename(FileLock lock, String name, String ext) throws IOException { MultiFileSystem fs = getMultiFileSystem (); if (parent == null) { FSException.io ("EXC_CannotRenameRoot", fs.getDisplayName ()); // NOI18N } synchronized (parent) { // synchronize on your folder testLock (lock); MfLock l = (MfLock)lock; String newFullName = parent.toString () + PATH_SEP + name; if (isData ()) { newFullName += EXT_SEP + ext; } String oldFullName = toString (); if (isReadOnly ()) { FSException.io ("EXC_CannotRename", toString (), getMultiFileSystem ().getDisplayName (), newFullName); // NOI18N } if (getFileSystem ().isReadOnly()) { FSException.io ("EXC_FSisRO", getMultiFileSystem ().getDisplayName ()); // NOI18N } String on = getName (); String oe = getExt (); //!!! getMultiFileSystem ().change.rename (oldFullName, newFullName); FileSystem single = fs.createWritableOn (newFullName); if (single == leader.getFileSystem ()) { // delete the file if we can on the selected // system leader.rename (l.lock, name, ext); } else { // rename file that is on different file system // means to copy it if (isData ()) { // data FileObject folder = FileUtil.createFolder(single.getRoot (), getParent ().toString ()); leader = leader.copy (folder, name, ext); } else { // folder FileObject fo = FileUtil.createFolder (single.getRoot (), newFullName); copyContent (this, fo); leader = fo; update (); } if (l.lock != null) { l.lock.releaseLock (); } l.lock = leader.lock (); } if (getMultiFileSystem ().delegates (oldFullName).hasMoreElements ()) { // if there is older version of the file // then we have to mask it MultiFileSystem.maskFile (single, oldFullName); updateFoldersLock (getParent ()); } String oldName = this.name; this.name = name; // clear cached full name this.fullName = null; /* System.out.println ("Resulting file is: " + toString ()); System.out.println ("Bedw file is: " + newFullName); System.out.println ("Name: " + name); System.out.println ("Old : " + oldName); */ parent.refresh (name, oldName); //!!! getMultiFileSystem ().attr.renameAttributes (oldFullName, newFullName); if (hasAtLeastOneListeners ()) { fileRenamed0 (new FileRenameEvent(this, on, oe)); } } } /* Delete this file. If the file is a folder and it is not empty then * all of its contents are also recursively deleted. * * @param lock the lock obtained by a call to {@link #lock} * @exception IOException if the file could not be deleted */ public void delete (FileLock lock) throws IOException { if (parent == null) { FSException.io ( "EXC_CannotDeleteRoot", getMultiFileSystem ().getDisplayName () // NOI18N ); } MultiFileSystem fs = getMultiFileSystem (); synchronized (parent) { testLock (lock); MfLock l = (MfLock)lock; String fullName = toString (); //!!! getMultiFileSystem ().change.delete (fullName); FileSystem single = fs.createWritableOn (fullName); if (single == leader.getFileSystem ()) { // delete the file if we can on the selected // system leader.delete (l.lock); } if (getMultiFileSystem ().delegates (fullName).hasMoreElements ()) { // if there is older version of the file // then we have to mask it MultiFileSystem.maskFile (single, fullName); updateFoldersLock (getParent ()); } String n = name; // if deleted set systemName to null, that indicates that // the object is not valid systemName = null; parent.refresh (null, n); //!!! getMultiFileSystem ().attr.deleteAttributes (fullName); if (hasAtLeastOneListeners ()) { fileDeleted0 (new FileEvent(this)); } } } // // Transfer // /** Copies this file. This allows the filesystem to perform any additional * operation associated with the copy. But the default implementation is simple * copy of the file and its attributes * * @param target target folder to move this file to * @param name new basename of file * @param ext new extension of file (ignored for folders) * @return the newly created file object representing the moved file */ public FileObject copy (FileObject target, String name, String ext) throws IOException { return leader.copy (target, name, ext); } /** Moves this file. This allows the filesystem to perform any additional * operation associated with the move. But the default implementation is encapsulated * as copy and delete. * * @param lock File must be locked before renaming. * @param target target folder to move this file to * @param name new basename of file * @param ext new extension of file (ignored for folders) * @return the newly created file object representing the moved file */ public FileObject move (FileLock lock, FileObject target, String name, String ext) throws IOException { MultiFileSystem fs = getMultiFileSystem (); if (parent == null) { FSException.io ( "EXC_CannotDeleteRoot", fs.getDisplayName () // NOI18N ); } FileLock l = testLock (lock); if (fs.isReadOnly() || l == null) { FSException.io ("EXC_FSisRO", fs.getDisplayName ()); // NOI18N } return leader.move (l, target, name, ext); } // // Listeners // /** Fired when a new folder is created. This action can only be * listened to in folders containing the created folder up to the root of * file system. * * @param fe the event describing context where action has taken place */ public void fileFolderCreated(FileEvent fe) { refresh (); } /** Fired when a new file is created. This action can only be * listened in folders containing the created file up to the root of * file system. * * @param fe the event describing context where action has taken place */ public void fileDataCreated(FileEvent fe) { refresh (); } /** Fired when a file is changed. * @param fe the event describing context where action has taken place */ public void fileChanged(FileEvent fe) { if (fe.getFile() == leader && hasAtLeastOneListeners ()) { fileChanged0 (new FileEvent (this)); } } /** Fired when a file is deleted. * @param fe the event describing context where action has taken place */ public void fileDeleted(FileEvent fe) { refresh (); } /** Fired when a file is renamed. * @param fe the event describing context where action has taken place * and the original name and extension. */ public void fileRenamed(FileRenameEvent fe) { refresh (); } /** Fired when a file attribute is changed. * @param fe the event describing context where action has taken place, * the name of attribute and the old and new values. */ public void fileAttributeChanged(FileAttributeEvent fe) { if (fe.getFile() == leader && hasAtLeastOneListeners ()) { fileAttributeChanged0 (new FileAttributeEvent ( this, fe.getName(), fe.getOldValue(), fe.getNewValue() )); } } /** Copies content of one folder into another. * @param source source folder * @param target target folder * @exception IOException if it fails */ private static void copyContent (FileObject source, FileObject target) throws IOException { FileObject[] srcArr = source.getChildren (); for (int i = 0; i < srcArr.length; i++) { FileObject child = srcArr[i]; if (child.isData ()) { FileUtil.copyFile (child, target, child.getName (), child.getExt ()); } else { FileObject targetChild = target.createFolder (child.getName ()); copyContent (child, targetChild); } } } /** Implementation of lock for abstract files. */ private class MfLock extends FileLock { private FileLock lock; public MfLock () { } public MfLock (FileLock l) { lock = l; } public void releaseLock () { if (this.isValid()) { super.releaseLock(); if (lock != null) { lock.releaseLock(); } MultiFileObject.this.lock = null; } } } // MfLock } /* * Log * 12 Gandalf 1.11 1/20/00 Jaroslav Tulach Menu on multiuser * instalation can be renamed. * 11 Gandalf 1.10 1/15/00 Jaroslav Tulach rename + delete of * folders and its content works. * 10 Gandalf 1.9 1/13/00 Ian Formanek NOI18N * 9 Gandalf 1.8 1/12/00 Ian Formanek NOI18N * 8 Gandalf 1.7 1/5/00 Jaroslav Tulach AbstractFileSystem.refreshResource * modifies lastModified time * 7 Gandalf 1.6 12/30/99 Jaroslav Tulach New dialog for * notification of exceptions. * 6 Gandalf 1.5 11/17/99 Jaroslav Tulach Works in multi * instalation. * 5 Gandalf 1.4 11/5/99 Jaroslav Tulach WeakListener has now * registration methods. * 4 Gandalf 1.3 11/3/99 Jaroslav Tulach Can create new files over * hidden ones. * 3 Gandalf 1.2 11/1/99 Jaroslav Tulach Folder is made "writable" * correctly * 2 Gandalf 1.1 11/1/99 Jaroslav Tulach Can lock files on * readonly fs too. * 1 Gandalf 1.0 10/29/99 Jaroslav Tulach * $ */